package ro.toporjinschi.androidorm;

import android.annotation.SuppressLint;
import java.lang.annotation.IncompleteAnnotationException;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import ro.toporjinschi.androidorm.annotation.Column;
import ro.toporjinschi.androidorm.annotation.Id;
import ro.toporjinschi.androidorm.annotation.NamedQueries;
import ro.toporjinschi.androidorm.annotation.NamedQuery;
import ro.toporjinschi.androidorm.annotation.TableName;
import ro.toporjinschi.androidorm.annotation.Transient;

final class EntityMetadata {
    public static final String HAS = "has";
    public static final String IS = "is";
    public static final String GET = "get";
    public static final String SET = "set";
    public static final String COMMA = ",";
    private Class<? extends Entity> clazz;
    private String tableName;
    // Database fieldName from ColumnAnnotation or the field name from bean value = Field from bean
    private final List<FieldMetaData> fields = new ArrayList<FieldMetaData>();
    private Map<String, String> queries = new HashMap<String, String>();
    private FieldMetaData idField;
    private String dbColumnsList;

    static EntityMetadata build(Class<? extends Entity> clz) {
        final EntityMetadata mapping = new EntityMetadata();
        mapping.clazz = clz;

        final TableName table = clz.getAnnotation(TableName.class);
        if (table == null) {
            throw new IncompleteAnnotationException(TableName.class, clz.getName());
        }
        mapping.tableName = table.value();
        mapping.queries = getNamedQueries(clz);
        final List<String> parsedFields = new ArrayList<String>();
        for (final Field field : clz.getDeclaredFields()) {
            if (parsedFields.contains(field.getName()) || field.isAnnotationPresent(Transient.class)) {
                continue;
            }
            final String fieldName = getFieldName(field);
            final FieldMetaData fmd = new FieldMetaData(field, field.getName(), fieldName.toLowerCase(), field.getType(), field.getAnnotation(Id.class) != null);
            fmd.setSetMethod(getSetMethod(clz, field));
            fmd.setGetMethod(getGetMethod(clz, field));
            mapping.fields.add(fmd);
            if (field.getAnnotation(Id.class) != null) {
                mapping.idField = fmd;
            }
            parsedFields.add(field.getName());
        }
        checkForIdField(mapping, clz.getName());
        mapping.dbColumnsList = getColumnList(mapping);
        return mapping;
    }

    private static Method getGetMethod(Class<? extends Entity> clz, Field field) {
        String fieldName = field.getName();
        fieldName = fieldName.substring(0, 1).toUpperCase() + fieldName.substring(1);
        try {
            return clz.getMethod(GET + fieldName);
        } catch (final NoSuchMethodException e) {
            try {
                return clz.getMethod(IS + fieldName);
            } catch (final NoSuchMethodException e1) {
                try {
                    return clz.getMethod(HAS + fieldName);
                } catch (final NoSuchMethodException e2) {
                    throw new RuntimeException("The get method for " + field.getName() + " cannot be found!");
                }
            }
        }
    }

    private static Method getSetMethod(Class<? extends Entity> clz, Field field) {
        String fieldName = field.getName();
        fieldName = fieldName.substring(0, 1).toUpperCase() + fieldName.substring(1);
        try {
            return clz.getMethod(SET + fieldName, field.getType());
        } catch (final NoSuchMethodException e) {
            throw new RuntimeException("The set method for " + field.getName() + " cannot be found!");
        }
    }

    private static void checkForIdField(EntityMetadata mapping, String className) {
        if (mapping.idField == null) {
            throw new IncompleteAnnotationException(Id.class, className);
        }
    }

    private static String getFieldName(Field field) {
        final Column col = field.getAnnotation(Column.class);
        String name;
        if (col != null) {
            name = col.name();
            if ("".equals(name)) {
                name = field.getName();
            }
        } else {
            name = field.getName();
        }
        return name;
    }

    private static Map<String, String> getNamedQueries(Class<? extends Entity> clz) {
        final Map<String, String> queries = new HashMap<String, String>();
        final NamedQueries namedQueries = clz.getAnnotation(NamedQueries.class);
        if (namedQueries == null) {
            return queries;
        }
        final NamedQuery[] queryList = namedQueries.value();
        if (queryList == null) {
            return queries;
        }
        for (final NamedQuery query : queryList) {
            queries.put(query.name(), query.sql());
        }
        return queries;
    }

    private static String getColumnList(EntityMetadata mapping) {
        final StringBuffer sb = new StringBuffer(100);

        for (int i = 0; i < mapping.fields.size(); i++) {
            final String dbField = mapping.fields.get(i).getDbName();
            sb.append(dbField);
            if (i < mapping.fields.size() - 1) {
                sb.append(COMMA);
            }
        }
        return sb.toString();
    }

    public String getDbColumnsList() {
        return this.dbColumnsList;
    }

    public String getTableName() {
        return this.tableName;
    }

    public Class<? extends Entity> getClazz() {
        return this.clazz;
    }

    public List<FieldMetaData> getFields() {
        return this.fields;
    }

    public String getIdDbName() {
        return this.idField.getDbName();
    }

    public Field getId() {
        return this.idField.getField();
    }

    public Object getIdValue(Entity instance) {
        try {
            return this.idField.getGetMethod().invoke(instance);
        } catch (final IllegalArgumentException e) {
            e.printStackTrace();
        } catch (final IllegalAccessException e) {
            e.printStackTrace();
        } catch (final InvocationTargetException e) {
            e.printStackTrace();
        }
        return null;
    }

    public FieldMetaData getIdField() {
        return this.idField;
    }

    public void setIdField(FieldMetaData idField) {
        this.idField = idField;
    }

    //    public Map<String, NamedQueryBean> getQueries() {
    //        return this.queries;
    //    }

    public String getQuery(String namedQuery) {
        return this.queries.get(namedQuery);
    }

    public Field getField(String dbName) {
        for (int i = 0; i < this.fields.size(); i++) {
            final String dbField = this.fields.get(i).getDbName();
            if (dbField.equals(dbName)) {
                return this.fields.get(i).getField();
            }
        }
        return null;
    }

    public FieldMetaData getFieldMetaData(String dbName) {
        for (int i = 0; i < this.fields.size(); i++) {
            final String dbField = this.fields.get(i).getDbName();
            if (dbField.equals(dbName)) {
                return this.fields.get(i);
            }
        }
        return null;
    }
}